package iamdev.me.ytds.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.plugins.pagination.Pagination;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import iamdev.me.ytds.config.BaseConfig;
import iamdev.me.ytds.entity.Doc;
import iamdev.me.ytds.entity.UploadFile;
import iamdev.me.ytds.mapper.DocMapper;
import iamdev.me.ytds.repository.DocRepository;
import iamdev.me.ytds.service.IDocService;
import iamdev.me.ytds.service.IThreadPoolExecutorService;
import iamdev.me.ytds.service.IUploadFileService;
import iamdev.me.ytds.utils.OfficeUtils;
import iamdev.me.ytds.utils.StringUtils;
import iamdev.me.ytds.utils.TikaUtils;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.DefaultEntityMapper;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author zxc
 * @since 2018-06-22
 */
@Service
public class DocServiceImpl extends ServiceImpl<DocMapper, Doc> implements IDocService {
    @Autowired
    private DocRepository docRepository;
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
    @Autowired
    private IUploadFileService uploadFileService;
    @Autowired
    private IThreadPoolExecutorService executorService;
    @Autowired
    private BaseConfig baseConfig;

    private Logger logger=LoggerFactory.getLogger(this.getClass());

    private final DefaultEntityMapper defaultEntityMapper;

    public DocServiceImpl() {
        defaultEntityMapper = new DefaultEntityMapper();
    }



    @Override
    public List<Doc>queryNeedIndexDoc(){
        EntityWrapper<Doc> ew=new EntityWrapper<>();
        ew.eq("doc_status",0);
        return baseMapper.selectList(ew);
    }

    @Override
    public List<Doc>queryNeedDeleteDoc(){
        EntityWrapper<Doc> ew=new EntityWrapper<>();
        ew.eq("doc_delete",1);
        return baseMapper.selectList(ew);
    }

    @Override
    public List<Doc>queryNeedConvertDoc(){
        EntityWrapper<Doc> ew=new EntityWrapper<>();
        ew.eq("doc_convert",0);
        ew.eq("source","file");
        ew.ne("doc_type","pdf");
        return baseMapper.selectList(ew);
    }


    @Override
    public void convertDocSync(Doc doc) {
        if(doc.getDocConvert()!=0){
            return;
        }
        logger.info("开始转换文档:{}",doc.getDocName());
        if(doc==null){
            logger.error("doc is null");
        }else {
            UploadFile uploadFile = uploadFileService.getUploadFile(doc.getDocFileId());
            if(uploadFile==null){
                logger.error("upload file not exists,docId:{}",doc.getDocId());
            }else {
                File file = new File(uploadFile.getFilePath());
                if(file.exists()){
                    File targetDir = new File(file.getParentFile(),"pdf");
                    boolean result = OfficeUtils.convertPdf(baseConfig.getSofficePath(),file,targetDir);
                    if(!result){
                        doc.setDocConvert(2);
                        this.updateById(doc);
                        logger.error("文档转换失败,{}",doc.getDocName());
                    }else {
                        doc.setDocConvert(1);
                        logger.info("文档转换成功,{}",doc.getDocName());
                        this.updateById(doc);
                    }
                }else {
                    logger.error("file not exists,docId:{}",doc.getDocId());
                }
            }
        }
    }



    @Override
    public void indexDocSync(Doc doc){

        logger.info("index Task docId:{},docName:{}",doc.getDocId(),doc.getDocName());
        if(doc==null){
            logger.error("doc is null");
        }else {
            UploadFile uploadFile = uploadFileService.getUploadFile(doc.getDocFileId());
            if(uploadFile==null){
                logger.error("upload file not exists,docId:{}",doc.getDocId());
            }else {
                File file = new File(uploadFile.getFilePath());
                if(file.exists()){
                    String content = TikaUtils.parseContent(file);
                    if(content==null){
                        doc.setDocStatus(2);
                        this.updateById(doc);
                        logger.error("tika解析文件内容出错,fileId:{}",file.getAbsolutePath());
                    }else {
                        doc.setDocIndex(1);
                        doc.setDocStatus(1);
                        doc.setDocContent(content);
                        doc.setId(doc.getDocId().toString());
                        //save doc to elastic
                        this.saveDocToEs(doc);
                        //文档内容不存到mysql
                        doc.setDocContent(null);
                        //update doc to mysql
                        this.updateById(doc);
                    }
                }else {
                    logger.error("file not exists,docId:{}",doc.getDocId());
                }
            }
        }

    }

    @Transactional
    @Override
    public void deleteDocSync(Doc doc){
        if(doc.getDocDelete()==1){
            logger.info("删除文档:{}", JSON.toJSONString(doc));
            docRepository.deleteById(doc.getDocId().toString());
            this.deleteById(doc);
            logger.info("删除转换的pdf文件");
            File pdfFile = getPdfFile(doc);
            if(pdfFile!=null && pdfFile.exists()){
                boolean deleted= pdfFile.delete();
                if(deleted){
                    logger.info("删除转换的pdf文档成功:{}",pdfFile.getAbsolutePath());
                }else {
                    logger.error("删除转换的pdf文档失败:{}",pdfFile.getAbsolutePath());
                }
            }
            String fileId = doc.getDocFileId();
            if(fileId!=null){
                uploadFileService.deleteUploadFile(fileId);
            }
        }
    }

    @Override
    @Transactional
    public void saveDoc(Integer userId, UploadFile uploadFile) {
        Doc doc =new Doc();
        doc.setDocTitle(uploadFile.getOriginalFileName());
        doc.setDocUserId(userId);
        doc.setDocType(StringUtils.getFileSuffix(uploadFile.getOriginalFileName()));
        doc.setDocSize(uploadFile.getFileSize());
        doc.setDocOpen(0);
        doc.setDocName(uploadFile.getOriginalFileName());
        doc.setDocModifyDate(new Date());
        doc.setDocDelete(0);
        doc.setDocCreateDate(new Date());
        doc.setDocFileId(uploadFile.getFileId());
        doc.setDocIndex(0);
        doc.setDocStatus(0);
        doc.setSource("file");
        doc.setDocSha256(uploadFile.getSha256());
        if(doc.getDocType().equals("pdf")){
            doc.setDocConvert(1);
        }else {
            doc.setDocConvert(0);
        }
        this.insert(doc);
        executorService.execute(() -> indexDocSync(doc));
        executorService.singleExecute(()-> convertDocSync(doc));

    }


    private void saveDocToEs(Doc doc){
        docRepository.save(doc);
    }

    @Override
    public Doc queryDocBySha256(String sha256, Integer userId){
        EntityWrapper<Doc>ew=new EntityWrapper<>();
        ew.eq("doc_sha256",sha256);
        ew.eq("doc_user_id",userId);
        List<Doc>docList = baseMapper.selectList(ew);
        if(docList.size()==0){
            return null;
        }else {
            return docList.get(0);
        }
    }




    @Override
    public File getPdfFile(Doc doc){
        if(doc.getDocFileId()!=null){
            UploadFile uploadFile = uploadFileService.getUploadFile(doc.getDocFileId());
            if(doc.getDocConvert()==1){
                String path = uploadFile.getFilePath();
                File srcFile =  new File(path);
                File dir = new File(srcFile.getParentFile(),"pdf");
                File pdfFile =new File(dir,uploadFile.getFileId()+".pdf");
                if(pdfFile.exists()) {
                    return pdfFile;
                }
            }
        }
        return null;
    }


    @Override
    public List<Doc> findDoc(Pagination pagination,Map<String, Object>params){
        return baseMapper.findDoc(pagination,params);
    }

    @Transactional
    @Override
    public void deleteDoc(Integer docId){
        Doc doc =new Doc();
        doc.setId(docId.toString());
        doc.setDocId(docId);
        doc.setDocDelete(1);
        baseMapper.updateById(doc);

        //update到es
        IndexRequest indexRequest = new IndexRequest();
        indexRequest.source("docDelete", 1);
        UpdateQuery updateQuery = new UpdateQueryBuilder().withId(doc.getId()).withClass(Doc.class).withIndexRequest(indexRequest).build();
        elasticsearchTemplate.update(updateQuery);
        Doc doc2 = baseMapper.selectById(docId);
        executorService.execute(()->{
            deleteDocSync(doc2);
        });
    }




    @Override
    public Map queryDocTypeNumber(Integer userId){
        return baseMapper.queryDocTypeNumber(userId);
    }

    private void completeDoc(List<Doc> docList){
        List<Integer>docIdList = docList.stream().map(Doc::getDocId).collect(Collectors.toList());
        EntityWrapper<Doc>ew=new EntityWrapper<>();
        ew.in("doc_id",docIdList);
        Map<Integer,Doc>map=new HashMap<>();
        baseMapper.selectList(ew).forEach(doc->{
            map.put(doc.getDocId(),doc);
        });
        docList.forEach(doc -> doc.setDocConvert(map.get(doc.getDocId()).getDocConvert()));
    }


    /**
    es搜索文档
     */
    @Override
    public List<Doc> searchDoc(Map<String, Object> params, int page, int size){

        Long dateStart =(Long) params.get("dateStart");
        Long dateEnd =(Long) params.get("dateEnd");
        String docType =(String) params.get("docType");
        Integer userId =(Integer) params.get("userId");
        //文档大小
        //0表示不限，1表示0Kb-128kb,2表示128kb-512kb,
        //3表示512kb-1mb,4表示1mb-5mb,5表示5mb以上
        Integer docSizeLevel =(Integer) params.get("docSizeLevel");
        String words  =(String) params.get("words");
        //搜索词位置 0表示所有位置，1表示在文档内容，2表示在文档标题
        Integer wordsLocation = (Integer)params.get("wordsLocation");
        NativeSearchQueryBuilder searchQuery = new NativeSearchQueryBuilder();

        BoolQueryBuilder query = QueryBuilders.boolQuery();
        BoolQueryBuilder filterQuery = QueryBuilders.boolQuery();

        List<QueryBuilder> filterCnd = filterQuery.must();
        if(dateStart!=null&&dateEnd!=null){
            filterCnd.add(QueryBuilders.rangeQuery("docModifyDate").gte(dateStart).lt(dateEnd));
        }
        if(docType!=null&&!docType.equals("all")){
            BoolQueryBuilder docTypeQuery = QueryBuilders.boolQuery();
            filterCnd.add(docTypeQuery);
            List<QueryBuilder> docTypeCnd = docTypeQuery.should();
            if(docType.equals("pdf")){
                docTypeCnd.add(QueryBuilders.termQuery("docType","pdf"));
            }
            if(docType.equals("powerpoint")){
                docTypeCnd.add(QueryBuilders.termQuery("docType","ppt"));
                docTypeCnd.add(QueryBuilders.termQuery("docType","pptx"));
            }
            if(docType.equals("word")){
                docTypeCnd.add(QueryBuilders.termQuery("docType","doc"));
                docTypeCnd.add(QueryBuilders.termQuery("docType","docx"));
            }
            if(docType.equals("excel")){
                docTypeCnd.add(QueryBuilders.termQuery("docType","xls"));
                docTypeCnd.add(QueryBuilders.termQuery("docType","xlsx"));
            }
            if(docType.equals("html")){
                docTypeCnd.add(QueryBuilders.termQuery("docType","html"));
            }
        }

        if(docSizeLevel!=null){
            Long sizeStart= null;
            Long sizeEnd = null;
            switch (docSizeLevel){
                case 1:
                    sizeStart = 0L;
                    sizeEnd = 1024*128L;
                    break;
                case 2:
                    sizeStart = 1024*128L;
                    sizeEnd = 1024*512L;
                    break;
                case 3:
                    sizeStart = 1024*512L;
                    sizeEnd = 1024*1024L;
                    break;
                case 4:
                    sizeStart = 1024*1024L;
                    sizeEnd = 1024*1024*5L;
                    break;
                case 5:
                    sizeStart = 1024*1024*5L;
                    break;
            }
            RangeQueryBuilder docSizeRange = null;
            if(sizeStart!=null){
                docSizeRange = QueryBuilders.rangeQuery("docSize");
                docSizeRange.from(sizeStart);
            }
            if(sizeEnd!=null){
                docSizeRange.to(sizeEnd);
            }
            if(docSizeRange!=null){
                filterCnd.add(docSizeRange);
            }
        }
        if(userId!=null){
            filterCnd.add(QueryBuilders.termQuery("docUserId",userId));
        }
        filterCnd.add(QueryBuilders.termQuery("docDelete",0));
        if(wordsLocation!=null){
            MultiMatchQueryBuilder search=null;
            if(wordsLocation==0){
                search = QueryBuilders.multiMatchQuery(words,"docTitle","docContent");
            }else if(wordsLocation==1){
                search = QueryBuilders.multiMatchQuery(words,"docContent");
            }else if(wordsLocation==2){
                search = QueryBuilders.multiMatchQuery(words,"docTitle");
            }
            if(search!=null){
                query.must(search);
            }
        }else {
            query.must(QueryBuilders.multiMatchQuery(words,"docTitle","docContent"));
        }

        query.filter(filterQuery);
        searchQuery.withQuery(query);
        searchQuery.withHighlightFields(
                new HighlightBuilder.Field("docContent").fragmentSize(400),//.highlighterType("fvh").fragmentSize(200).numOfFragments(10),
                new HighlightBuilder.Field("docTitle")//.highlighterType("fvh")
        );
        NativeSearchQuery nativeSearchQuery = searchQuery.build();
        logger.info("query cnd:{}",nativeSearchQuery.getQuery());
        nativeSearchQuery.addSourceFilter(new FetchSourceFilter(new String[]{"docSize","docTitle","docType","docCreateDate","docId"},null));
        Pageable pageable =PageRequest.of(page,size);
        nativeSearchQuery.setPageable(pageable);
        Page<Doc> docPage = elasticsearchTemplate.queryForPage(nativeSearchQuery, Doc.class, new SearchResultMapper() {
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                List<Doc> chunk = new ArrayList<>();
                for (SearchHit searchHit : response.getHits()) {
                    if (response.getHits().getHits().length <= 0) {
                        return null;
                    }
                    logger.info("score:{}",searchHit.getScore());
                    chunk.add(searchHitToDoc(searchHit));
                }
                if (chunk.size() > 0) {
                    return new AggregatedPageImpl<>((List<T>) chunk);
                }
                return null;
            }
        });
        if(docPage==null){
            return new ArrayList<>();
        }
        List<Doc>docList =  StreamSupport.stream(docPage.spliterator(),false).collect(Collectors.toList());
        completeDoc(docList);
        return docList;
    }

    private Doc searchHitToDoc(SearchHit searchHit) {
        Doc doc;
        try {
            doc = defaultEntityMapper.mapToObject(searchHit.getSourceAsString(), Doc.class);
        } catch (IOException e) {
            throw new ElasticsearchException("failed to map source [ " + searchHit.getSourceAsString() + "] to class " + Doc.class.getSimpleName(), e);
        }
        HighlightField docContentHighlightField=searchHit.getHighlightFields().get("docContent");
        HighlightField docTitleHighlightField =searchHit.getHighlightFields().get("docTitle");
        if(docContentHighlightField!=null){
            List<String> docContentHighLight = Arrays.stream(docContentHighlightField.fragments()).map(Text::string).collect(Collectors.toList());
            StringBuilder sb =new StringBuilder();
            docContentHighLight.stream().forEach(sb::append);
            doc.setDocContent(sb.toString());
        }
        if(docTitleHighlightField!=null){
            List<String> docTitleHighLight = Arrays.stream(docTitleHighlightField.fragments()).map(Text::string).collect(Collectors.toList());
            StringBuilder sb =new StringBuilder();
            docTitleHighLight.stream().forEach(sb::append);
            doc.setDocTitle(sb.toString());
        }
        return doc;
    }

}
